/*
 * Created on Feb 1, 2013
 * Created by Paul Gardner
 * 
 * Copyright 2013 Azureus Software, Inc.  All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */


package torrentlib.stats;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.*;

import controller.config.COConfigurationManager;
import controller.config.ParameterListener;
import controller.GlobalManagerStats;
import torrentlib.stats.LongTermStatsListener;
import torrentlib.stats.StatsFactory;
import torrentlib.stats.LongTermStatsWrapper.LongTermStatsWrapperHelper;
import torrentlib.AERunnable;
import torrentlib.AsyncDispatcher;
import torrentlib.Debug;
import torrentlib.util.FileUtil;
import torrentlib.SimpleTimer;
import torrentlib.SystemTime;
import torrentlib.TimerEvent;
import torrentlib.TimerEventPerformer;
import torrentlib.TimerEventPeriodic;
import torrentlib.util.platform.win32.PluginInterface;
import torrentlib.util.platform.win32.PluginManager;

import torrentlib.AzureusCoreComponent;
import torrentlib.AzureusCoreLifecycleAdapter;
import connect.dht.DHT;
import connect.dht.transport.DHTTransportStats;
import torrentlib.CopyOnWriteList;
import stdlib.math.average.Average;
import stdlib.math.average.AverageFactory;
import plugins.dht.DHTPlugin;
import controller.GlobalManager;
import torrentlib.TorrentEngineCore;

public class 
LongTermStatsImpl 
	implements LongTermStatsWrapperHelper
{
	private static final int VERSION = 1;
	
	private static final long MIN_IN_MILLIS		= 60*1000;
	private static final long HOUR_IN_MILLIS	= 60*60*1000;
	private static final long DAY_IN_MILLIS		= 24*60*60*1000;
	private static final long WEEK_IN_MILLIS	= 7*24*60*60*1000;

	public static final int RT_SESSION_START	= 1;
	public static final int RT_SESSION_STATS	= 2;
	public static final int RT_SESSION_END		= 3;
	
	private TorrentEngineCore			core;
	private GlobalManagerStats	gm_stats;
	private DHT[]				dhts;
	
	private final int STAT_ENTRY_COUNT	= 6;
	
		// totals at start of session
	
	private long	st_p_sent;
	private long	st_d_sent;
	private long	st_p_received;
	private long	st_d_received;
	private long	st_dht_sent;
	private long	st_dht_received;
	
		// session offsets at start of session
	
	private long	ss_p_sent;
	private long	ss_d_sent;
	private long	ss_p_received;
	private long	ss_d_received;
	private long	ss_dht_sent;
	private long	ss_dht_received;
	
	private long[] line_stats_prev = new long[STAT_ENTRY_COUNT];
	
	private Average[] stat_averages = new Average[STAT_ENTRY_COUNT];

	{
		for ( int i=0;i<STAT_ENTRY_COUNT;i++){
			
			stat_averages[i] = AverageFactory.MovingImmediateAverage( 3 );
		}
	}
	
	private boolean				active;
	private boolean				closing;
	
	private TimerEventPeriodic	event;
	private PrintWriter			writer;
	private String				writer_rel_file;
	
	private DayCache			day_cache;
	
	private final int MONTH_CACHE_MAX = 3;
	
	private Map<String,MonthCache>	month_cache_map = 
		new LinkedHashMap<String,MonthCache>(MONTH_CACHE_MAX,0.75f,true)
		{
			protected boolean 
			removeEldestEntry(
		   		Map.Entry<String,MonthCache> eldest) 
			{
				return size() > MONTH_CACHE_MAX;
			}
		};
		
	private static SimpleDateFormat	debug_utc_format 	= new SimpleDateFormat( "yyyy,MM,dd:HH:mm" );
	private static SimpleDateFormat	utc_date_format 	= new SimpleDateFormat( "yyyy,MM,dd" );
	
	static{
		debug_utc_format.setTimeZone( TimeZone.getTimeZone( "UTC" ));
		utc_date_format.setTimeZone( TimeZone.getTimeZone( "UTC" ));
	}
	
	private final File stats_dir;
	
	private long	session_total;
	
	private CopyOnWriteList<Object[]>	listeners = new CopyOnWriteList<Object[]>();
	
	private AsyncDispatcher	dispatcher = new AsyncDispatcher( "lts", 5000 );
	
	private int	start_of_week 	= -1;
	private int start_of_month	= -1;
	
	private volatile boolean	destroyed;
	
	private
	LongTermStatsImpl(
		File	_stats_dir )
	{	
		stats_dir = _stats_dir;
	}
	
	public
	LongTermStatsImpl(
		TorrentEngineCore			_core,
		GlobalManagerStats	_gm_stats )
	{
		core		= _core;
		gm_stats 	= _gm_stats;
		
		stats_dir		= FileUtil.getUserFile( "stats" );
		
		COConfigurationManager.addParameterListener(
			"long.term.stats.enable",
			new ParameterListener()
			{
				public void 
				parameterChanged(
					String name) 
				{
					if ( destroyed ){
						
						COConfigurationManager.removeParameterListener( "long.term.stats.enable", this );
						
						return;
					}
					
					boolean	enabled = COConfigurationManager.getBooleanParameter( name );
					
					synchronized( LongTermStatsImpl.this ){
												
						if ( enabled ){
							
							if ( !active ){
								
								sessionStart();
							}
						}else{
							
							if ( active ){
								
								sessionEnd();
							}
						}
					}
				}
			});
		
	    _core.addLifecycleListener(new AzureusCoreLifecycleAdapter()
	        	{
	        		public void
	        		componentCreated(
	        			TorrentEngineCore				core,
	        			AzureusCoreComponent	component )
	        		{
	        			if ( destroyed ){
	        				
	        				core.removeLifecycleListener( this );
	        				
	        				return;
	        			}
	        			
	        			if ( component instanceof GlobalManager ){
	        				
	        				synchronized( LongTermStatsImpl.this ){
	        					
	        					sessionStart();
	        				}
	        			}
	        		}
	        		
	        		public void
	        		stopped(
	        			TorrentEngineCore		core )
	        		{
	        			if ( destroyed ){
	        				
	        				core.removeLifecycleListener( this );
	        				
	        				return;
	        			}

	        			synchronized( LongTermStatsImpl.this ){
	        				
	        				closing	= true;
	        				
	        				if ( active ){
	        			
	        					sessionEnd();
	        				}
	        			}
	        		}
	        	});
	}
	
	public boolean
	isEnabled()
	{
		synchronized( this ){
		
			return( active );
		}
	}
	
	public void
	reset()
	{
		Debug.out( "eh?" );
	}
	
	public void 
	destroyAndDeleteData() 
	{
		synchronized( this ){
		
			destroyed = true;
			
			if ( writer != null ){
				
				writer.close();
				
				writer = null;
			}
			
			File[] files = stats_dir.listFiles();
			
outer:
			for ( File file: files ){
				
				String name = file.getName();
				
				if ( name.length() == 4 && Character.isDigit( name.charAt(0))){
					
					for ( int i=0;i<4;i++){
						
						if ( FileUtil.recursiveDeleteNoCheck( file )){
							
							continue outer;
						}
						
						try{
							Thread.sleep( 250 );
							
						}catch( Throwable e ){
						}
					}
					
					Debug.out( "Failed to delete " + file );
				}
			}
		}
	}
	
	private DHT[]
	getDHTs()
	{
	    if ( dhts == null ){
	    	
		    try{
		    	PluginManager pm = core.getPluginManager();
		    	
		    	if ( pm.isInitialized()){

			        PluginInterface dht_pi = pm.getPluginInterfaceByClass( DHTPlugin.class );
			          
			        if ( dht_pi == null ){
			           
			        	dhts = new DHT[0];
			        	
			        }else{
			        	
			        	DHTPlugin plugin = (DHTPlugin)dht_pi.getPlugin();
			        	
			        	if ( !plugin.isInitialising()){
			        		
				        	if ( plugin.isEnabled()){
				        	
				        		dhts = ((DHTPlugin)dht_pi.getPlugin()).getDHTs();
				        		
				        	}else{
				        		
				        		dhts = new DHT[0];
				        	}
			        	}
			        }
		    	}
		    }catch( Throwable e ){
		    	
		    	dhts = new DHT[0];
		    }
	    }
	    
	    return( dhts );
	}
	
	private void
	sessionStart()
	{
		OverallStatsImpl stats = (OverallStatsImpl)StatsFactory.getStats();

		synchronized( this ){
			
			if ( closing ){
				
				return;
			}
			
			boolean	enabled = COConfigurationManager.getBooleanParameter( "long.term.stats.enable" );

			if ( active || !enabled ){
				
				return;
			}
			
			active = true;
			
			long[] snap = stats.getLastSnapshot();
			
			ss_d_received 	= gm_stats.getTotalDataBytesReceived();
			ss_p_received 	= gm_stats.getTotalProtocolBytesReceived();

			ss_d_sent		= gm_stats.getTotalDataBytesSent();
			ss_p_sent		= gm_stats.getTotalProtocolBytesSent();
			    
			ss_dht_sent		= 0;
			ss_dht_received	= 0;
			
			if ( core.isStarted()){
				
				DHT[]	dhts = getDHTs();
				
				if ( dhts != null ){
					
					for ( DHT dht: dhts ){
						
				    	DHTTransportStats dht_stats = dht.getTransport().getStats();
				    	
				    	ss_dht_sent 		+= dht_stats.getBytesSent();
				    	ss_dht_received 	+= dht_stats.getBytesReceived();
					}
				}
			}
			
			st_p_sent 		= snap[0] + ( ss_p_sent - snap[6]);
			st_d_sent 		= snap[1] + ( ss_d_sent - snap[7]);
			st_p_received 	= snap[2] + ( ss_p_received - snap[8]);
			st_d_received 	= snap[3] + ( ss_d_received - snap[9]);
			st_dht_sent		= snap[4] + ( ss_dht_sent - snap[10]);
			st_dht_received = snap[5] + ( ss_dht_received - snap[11]);
			
			write( 	RT_SESSION_START,  
					new long[]{ 
						st_p_sent,
						st_d_sent,
						st_p_received,
						st_d_received,
						st_dht_sent,
						st_dht_received });
			
			if ( event == null ){	// should always be null but hey ho
				
			    event = 
			    	SimpleTimer.addPeriodicEvent(
				    	"LongTermStats", 
				    	MIN_IN_MILLIS, 
				    	new TimerEventPerformer()
				    	{
				    		public void 
				    		perform(TimerEvent event) 
				    		{
				    			if ( destroyed ){
				    				
				    				event.cancel();
				    				
				    				return;
				    			}
				    			
				    			updateStats();
				    		}
				    	});
			}
		}
	}
	
	private void
	sessionEnd()
	{
		synchronized( this ){
			
			if ( !active ){
				
				return;
			}
			
			updateStats( RT_SESSION_END );

			active = false;
			
			if ( event != null ){
				
				event.cancel();
				
				event = null;
			}
		}
	}
	
	private void
	updateStats()
	{
		updateStats( RT_SESSION_STATS );
	}
	
	private void
	updateStats(
		int	record_type )
	{	    
	    long	current_d_received 	= gm_stats.getTotalDataBytesReceived();
	    long	current_p_received 	= gm_stats.getTotalProtocolBytesReceived();

	    long	current_d_sent		= gm_stats.getTotalDataBytesSent();
	    long	current_p_sent		= gm_stats.getTotalProtocolBytesSent();
   
		long	current_dht_sent		= 0;
		long	current_dht_received	= 0;

		DHT[]	dhts = getDHTs();
		
		if ( dhts != null ){
			
			for ( DHT dht: dhts ){
				
		    	DHTTransportStats dht_stats = dht.getTransport().getStats();
		    	
		    	current_dht_sent 		+= dht_stats.getBytesSent();
		    	current_dht_received 	+= dht_stats.getBytesReceived();
			}
		}
		
	    write(	record_type,
	    		new long[]{
		    		( current_p_sent - ss_p_sent ),
		    		( current_d_sent - ss_d_sent ),
		    		( current_p_received - ss_p_received ),
		    		( current_d_received - ss_d_received ),
		    		( current_dht_sent - ss_dht_sent ),
		    		( current_dht_received - ss_dht_received )});
	}
		
	private void
	write(
		int		record_type,
		long[]	line_stats )
	{
		synchronized( this ){

			if ( destroyed ){
				
				return;
			}
			
			try{
				final long	now = SystemTime.getCurrentTime();
				
				final long	now_mins = now/(1000*60);
				
				String[] bits = utc_date_format.format( new Date( now )).split( "," );
				
				String	year 	= bits[0];
				String	month 	= bits[1];
				String	day 	= bits[2];
				
				String	current_rel_file = year + File.separator + month + File.separator + day + ".dat";

				String line;
				
				String stats_str = "";
				
				if ( record_type == RT_SESSION_START ){
					
						// absolute values
					
					for ( int i=0;i<line_stats.length;i++ ){
						
						stats_str += "," + line_stats[i];
						
						line_stats_prev[i] = 0;
					}
					
					day_cache = null;
					
				}else{
					
						// relative values
					
					long[]	diffs = new long[STAT_ENTRY_COUNT];
					
					for ( int i=0;i<line_stats.length;i++ ){
						
						long diff = line_stats[i] - line_stats_prev[i];
						
						session_total += diff;
						
						diffs[i] = diff;
						
						stats_str += "," + diff;
						
						line_stats_prev[i] = line_stats[i];
						
						stat_averages[i].update( diff );
					}
					
					if ( day_cache != null ){
						
						if ( day_cache.isForDay( year, month, day )){
							
							day_cache.addRecord( now_mins, diffs );
						}
					}
				}
				
				if ( record_type != RT_SESSION_STATS ){
					
					line = (record_type==RT_SESSION_START?"s,":"e,") + VERSION + "," + now_mins + stats_str;
					
				}else{
					
					line = stats_str.substring(1);
				}
				
				
				if ( writer == null || !writer_rel_file.equals( current_rel_file )){
				
						// first open of a file or file switch
					
					if ( writer != null ){
					
							// file switch
						
						if ( record_type != RT_SESSION_START ){
							
							writer.println( line );
						}
						
						writer.close();
						
						if ( writer.checkError()){
							
							writer 			= null;
							
							throw( new IOException( "Write faled" ));
						}
						
						writer 			= null;
					}
					
						// no point in opening a new file just to record the session-end
					
					if ( record_type != RT_SESSION_END ){
						
						File file = new File( stats_dir, current_rel_file );
						
						file.getParentFile().mkdirs();
						
						writer = new PrintWriter( new FileWriter( file, true ));
						
						writer_rel_file = current_rel_file;
							
						if ( record_type == RT_SESSION_START ){
						
							writer.println( line );
							
						}else{
							
								// first entry in a new file, files always start with a session-start so they
								// can be processed in isolation so reset the session data and start a new one
							
							st_p_sent		+= line_stats[0];
							st_d_sent		+= line_stats[1];
							st_p_received	+= line_stats[2];
							st_d_received	+= line_stats[3];
							st_dht_sent		+= line_stats[4];
							st_dht_received	+= line_stats[5];
							
							ss_p_sent		+= line_stats[0];
							ss_d_sent		+= line_stats[1];
							ss_p_received	+= line_stats[2];
							ss_d_received	+= line_stats[3];
							ss_dht_sent		+= line_stats[4];
							ss_dht_received	+= line_stats[5];
														
							stats_str = "";
							
							long[] st_stats =  new long[]{ st_p_sent,st_d_sent, st_p_received,st_d_received, st_dht_sent, st_dht_received };
							
							for ( int i=0;i<st_stats.length; i++ ){
								
								stats_str += "," + st_stats[i];
								
								line_stats_prev[i] = 0;
							}
							
							line = "s," + VERSION + "," + now_mins + stats_str;
						
							writer.println( line );
						}
					}
				}else{
					
					writer.println( line );
				}
								
			}catch( Throwable e ){
				
				Debug.out( "Failed to write long term stats", e );
				
			}finally{
				
				if ( writer != null ){
					
					if ( record_type == RT_SESSION_END ){
						
						writer.close();
					}
					
					if ( writer.checkError()){
						
						Debug.out( "Failed to write long term stats" );
						
						writer.close();
						
						writer	= null;
						
					}else{
						
						if ( record_type == RT_SESSION_END ){
					
							writer	= null;
						}
					}
				}
			}
		}
		
		if ( record_type != RT_SESSION_END ){
			
			final List<LongTermStatsListener> to_fire = new ArrayList<LongTermStatsListener>();
			
			for ( Object[] entry: listeners ){
				
				long	diff = session_total - (Long)entry[2];
				
				if ( diff >= (Long)entry[1]){
					
					entry[2] = session_total;
					
					to_fire.add((LongTermStatsListener)entry[0]);
				}
			}
			
			if ( to_fire.size() > 0 ){
				
				dispatcher.dispatch(
					new AERunnable()
					{
						@Override
						public void 
						runSupport() 
						{
							for ( LongTermStatsListener l: to_fire ){
								
								try{
									l.updated( LongTermStatsImpl.this );
									
								}catch( Throwable e ){
									
									Debug.out( e );
								}
							}
						}
					});
			}
		}
	}
	
	private static String
	getString(
		long[] stats )
	{
		String str = "";
		
		for ( long s: stats ){
			
			str += (str.length()==0?"":", ") + s;
		}
		
		return( str );
	}
	
	private MonthCache
	getMonthCache(
		String	year,
		String	month )
	{
		String	key = year + "_" + month;
		
		MonthCache cache = month_cache_map.get( key );
		
		if ( cache == null ){
			
			cache = new MonthCache( year, month );
			
			month_cache_map.put( key, cache );
		}
		
		return( cache );
	}
	
	public long[]
	getTotalUsageInPeriod(
		Date				start_date,
		Date				end_date )
	{
		return( getTotalUsageInPeriod( start_date, end_date, null ));
	}
	
	public long[]
	getTotalUsageInPeriod(
		Date				start_date,
		Date				end_date,
		RecordAccepter		accepter )
	{
		boolean	enable_caching = accepter == null;
		
		synchronized( this ){
			
			long[] result = new long[STAT_ENTRY_COUNT];
			
			long start_millis 	= start_date.getTime();
			long end_millis 	= end_date.getTime();
			
			long	now = SystemTime.getCurrentTime();
			
			long	now_day	= (now/DAY_IN_MILLIS)*DAY_IN_MILLIS;
			
			if ( end_millis > now ){
				
				end_millis = now;
			}
			
			long start_day 	= (start_millis/DAY_IN_MILLIS)*DAY_IN_MILLIS;
			long end_day 	= (end_millis/DAY_IN_MILLIS)*DAY_IN_MILLIS;
			
			if ( start_day > end_day ){
				
				return( result );
			}
			
			long start_offset = start_millis - start_day;
			
			start_offset = start_offset/MIN_IN_MILLIS;
			
			boolean	offset_cachable = start_offset % 60 == 0;
			
			//System.out.println( "start=" + debug_utc_format.format( start_date ) + ", end=" + debug_utc_format.format( end_date ) + ", offset=" + start_offset);
			
			MonthCache	month_cache = null;
			
			for ( long this_day=start_day;this_day<=end_day;this_day+=DAY_IN_MILLIS ){
				
				String[] bits = utc_date_format.format( new Date( this_day )).split( "," );
				
				String year_str 	= bits[0];
				String month_str	= bits[1];
				String day_str		= bits[2];
				
				int	year 	= Integer.parseInt( year_str );
				int	month	= Integer.parseInt( month_str );
				int	day		= Integer.parseInt( day_str );
				
				long	cache_offset = this_day == start_day?start_offset:0;
				boolean	can_cache;
				
				if ( enable_caching ){
					
					if ( month_cache == null || !month_cache.isForMonth( year_str, month_str )){
						
						if ( month_cache != null && month_cache.isDirty()){
							
							month_cache.save();
						}
						
						month_cache = getMonthCache( year_str, month_str );
					}
					
					can_cache = 
						this_day != now_day &&
						( this_day > start_day || ( this_day == start_day && offset_cachable )) &&
						this_day < end_day;
										
					if ( can_cache ){
						
						long[] cached_totals = month_cache.getTotals( day, cache_offset );
						
						if ( cached_totals != null ){
							
							for ( int i=0;i<cached_totals.length;i++){
								
								result[i] += cached_totals[i];
							}
							
							continue;
						}
					}else{
						
						if ( this_day == now_day ){
							
							if ( day_cache != null ){
								
								if ( day_cache.isForDay( year_str, month_str, day_str )){
									
									long[] cached_totals = day_cache.getTotals( cache_offset );
									
									if ( cached_totals != null ){
										
										for ( int i=0;i<cached_totals.length;i++){
											
											result[i] += cached_totals[i];
										}
										
										continue;
									}
									
								}else{
									
									day_cache = null;
								}
							}
						}
					}
				}else{
					
					can_cache = false;
				}
				
				String	current_rel_file = bits[0] + File.separator + bits[1] + File.separator + bits[2] + ".dat";
					
				File stats_file = new File( stats_dir, current_rel_file );
				
				if ( !stats_file.exists()){
					
					if ( can_cache ){
						
						month_cache.setTotals( day, cache_offset, new long[0] );
					}
				}else{
					
					LineNumberReader lnr = null;
					
					try{
						//System.out.println( "Reading " + stats_file );
						
						lnr = new LineNumberReader( new FileReader( stats_file ));
						
						long	file_start_time	= 0;
						
						long[]	file_totals = null;
						
						long[]	file_result_totals  = new long[STAT_ENTRY_COUNT];
						
						long[]	session_start_stats = null;
						long	session_start_time	= 0;
						long	session_time		= 0;
						
						while( true ){
							
							String line = lnr.readLine();
							
							if ( line == null ){
								
								break;
							}
							
							//System.out.println( line );
							
							String[] fields = line.split( "," );
							
							if ( fields.length < 6 ){
								
								continue;
							}
							
							try{
								String first_field = fields[0];
								
								if ( first_field.equals("s")){
																
									session_start_time = Long.parseLong( fields[2] )*MIN_IN_MILLIS;
									
									if ( file_totals == null ){
										
										file_totals = new long[STAT_ENTRY_COUNT];
										
										file_start_time = session_start_time;
									}
		
									session_time = session_start_time;
									
									session_start_stats = new long[STAT_ENTRY_COUNT];
									
									for ( int i=3;i<9;i++){
										
										session_start_stats[i-3] = Long.parseLong( fields[i] );
									}
								}else if ( session_start_time > 0 ){
									
									session_time += MIN_IN_MILLIS;
									
									int	field_offset = 0;
									
									if ( first_field.equals( "e" )){
										
										field_offset = 3;
									}
									
									long[] line_stats = new long[STAT_ENTRY_COUNT];
									
									for ( int i=0;i<6;i++){
										
										line_stats[i] = Long.parseLong( fields[i+field_offset] );
										
										file_totals[i] += line_stats[i];
									}
									
									if ( 	session_time >= start_millis && 
											session_time <= end_millis ){
										
										if ( accepter == null || accepter.acceptRecord( session_time )){
											
											for ( int i=0;i<6;i++){
												
												result[i] += line_stats[i];
												
												file_result_totals[i] += line_stats[i];
											}
										}
									}
									
									//System.out.println( getString( line_stats ));
								}
							}catch( Throwable e ){
								
								// skip the record, not much else that can be done
							}
						}
						
						if ( file_totals == null ){
							
							file_totals = new long[0];
						}
						
						//System.out.println( "File total: start=" + debug_utc_format.format(file_start_time) + ", end=" + debug_utc_format.format(session_time) + " - " + getString( file_totals ));
						
						if ( can_cache ){
							
							month_cache.setTotals( day, cache_offset, file_result_totals );
							
							if ( cache_offset != 0 ){
								
								month_cache.setTotals( day, 0, file_totals );
							}
						}else{
							
							if ( enable_caching ){
								
								if ( this_day == now_day ){
									
									if ( day_cache == null ){
										
										//System.out.println( "Creating day cache" );
										
										day_cache = new DayCache( year_str, month_str, day_str );
									}
									
									day_cache.setTotals( cache_offset, file_result_totals );
									
									if ( cache_offset != 0 ){
										
										day_cache.setTotals( 0, file_totals );
									}
								}
							}
						}
						
					}catch( Throwable e ){
						
						Debug.out( e );
						
					}finally{
						
						if ( lnr != null ){
							
							try{
								lnr.close();
								
							}catch( Throwable e ){
							}
						}
					}
				}
			}
			
			if ( enable_caching ){
				
				if ( month_cache != null && month_cache.isDirty()){
					
					month_cache.save();
				}
			}
			
			//System.out.println( "    -> " + getString( result ));
			
			return( result );
		}
	}
	
	public long[]
	getTotalUsageInPeriod(
		int		period_type,
		double	multiplier )
	{
		return( getTotalUsageInPeriod( period_type, multiplier, null ));
	}
	
	public long[]
	getTotalUsageInPeriod(
		int					period_type,
		double				multiplier, 
		RecordAccepter		accepter )
	{
		if ( start_of_week == -1 ){
			
			COConfigurationManager.addAndFireParameterListeners(
				new String[]{ "long.term.stats.weekstart", "long.term.stats.monthstart" },
				new ParameterListener()
				{
					public void 
					parameterChanged(
						String name )
					{
						start_of_week 	= COConfigurationManager.getIntParameter( "long.term.stats.weekstart" );
						start_of_month 	= COConfigurationManager.getIntParameter( "long.term.stats.monthstart" );
					}
				});
		}
		
		long	now = SystemTime.getCurrentTime();
		
		long top_time;
		long bottom_time;
		
		if ( period_type == PT_CURRENT_HOUR ){
			
			bottom_time = (now/HOUR_IN_MILLIS)*HOUR_IN_MILLIS;
			top_time	= bottom_time + HOUR_IN_MILLIS - 1;
		
		}else if ( period_type == PT_SLIDING_HOUR ){
			
			bottom_time = now - (long)(HOUR_IN_MILLIS*multiplier);
			top_time	= now;
			
		}else if ( period_type == PT_SLIDING_DAY ){
			
			bottom_time = now - (long)(DAY_IN_MILLIS*multiplier);
			top_time	= now;
			
		}else if ( period_type == PT_SLIDING_WEEK ){
			
			bottom_time = now - (long)(WEEK_IN_MILLIS*multiplier);
			top_time	= now;
			
		}else{
			
			Calendar calendar = new GregorianCalendar();
			
			calendar.setTimeInMillis( SystemTime.getCurrentTime());
			
			calendar.set( Calendar.MILLISECOND, 0 );
			calendar.set( Calendar.MINUTE, 0 );
			calendar.set( Calendar.HOUR_OF_DAY, 0 );
			
			top_time = calendar.getTimeInMillis() + DAY_IN_MILLIS - 1;
			
			if ( period_type == PT_CURRENT_DAY ){
				
			}else if ( period_type == PT_CURRENT_WEEK ){
				
					// sun = 1, mon = 2 etc
				
				int day_of_week = calendar.get( Calendar.DAY_OF_WEEK );
				
				if ( day_of_week == start_of_week ){
					
				}else if ( day_of_week > start_of_week ){
							
					calendar.add( Calendar.DAY_OF_WEEK, - ( day_of_week - start_of_week ));
					
				}else{
					
					calendar.add( Calendar.DAY_OF_WEEK, - ( 7 - ( start_of_week - day_of_week )));
				}
				
			}else{
				
				if ( start_of_month == 1 ){
				
					calendar.set( Calendar.DAY_OF_MONTH, 1 );
					
				}else{
					
					int day_of_month = calendar.get( Calendar.DAY_OF_MONTH );
					
					if ( day_of_month == start_of_month ){
						
					}else if ( day_of_month > start_of_month ){
						
						calendar.set( Calendar.DAY_OF_MONTH, start_of_month );
						
					}else{
						
						calendar.add( Calendar.MONTH, -1 );
						
						calendar.set( Calendar.DAY_OF_MONTH, start_of_month );
					}
				}
			}
			
			bottom_time = calendar.getTimeInMillis();
		}
		
		return( getTotalUsageInPeriod( new Date( bottom_time ), new Date( top_time ), accepter ));
	}
	
	public long[] 
	getCurrentRateBytesPerSecond() 
	{
		long[] result = new long[STAT_ENTRY_COUNT];
		
		for ( int i=0;i<STAT_ENTRY_COUNT;i++){
			
			result[i] = (long)( stat_averages[i].getAverage()/60 );
		}
		
		return( result );
	}
	
	public void
	addListener(
		long							min_delta_bytes,
		final LongTermStatsListener		listener )
	{
		listeners.add( new Object[]{ listener, min_delta_bytes, session_total });
		
		dispatcher.dispatch(
			new AERunnable()
			{
				public void
				runSupport()
				{
					listener.updated( LongTermStatsImpl.this );
				}
			});
	}
	
	public void
	removeListener(
		LongTermStatsListener		listener )
	{
		for ( Object[] entry: listeners ){
			
			if ( entry[0] == listener ){
				
				listeners.remove( entry );
				
				break;
			}
		}
	}
	
	private class
	DayCache
	{
		private String			year;
		private String			month;
		private String			day;
		
		private Map<Long,long[]>	contents = new HashMap<Long, long[]>();

		private
		DayCache(
			String		_year,
			String		_month,
			String		_day )
		{
			year	= _year;
			month	= _month;
			day		= _day;
		}
		
		private boolean
		isForDay(
			String	_year,
			String	_month,
			String	_day )
		{
			return( year.equals( _year ) && month.equals( _month ) && day.equals( _day ));
		}
		
		private void
		addRecord(
			long	offset,
			long[]	stats )
		{
			for ( Map.Entry<Long,long[]> entry: contents.entrySet()){
				
				if ( offset >= entry.getKey()){
					
					long[] old = entry.getValue();
					
					for ( int i=0;i<old.length;i++){
						
						old[i] += stats[i];
					}
				}
			}
		}
		
		private long[]
		getTotals(
			long	offset )
		{
			return( contents.get( offset ));
		}
		
		private void
		setTotals(
			long	offset,
			long[]	value )
		{
			contents.put( offset, value );
		}
	}
	
	private class
	MonthCache
	{	
		private String			year;
		private String			month;
	
		private boolean		dirty;
		
		private Map<String,List<Long>>	contents;
		
		private
		MonthCache(
			String		_year,
			String		_month )
		{
			year	= _year;
			month	= _month;
		}
	
		private File
		getCacheFile()
		{
			return( new File( stats_dir, year + File.separator + month + File.separator + "cache.dat" ));
		}
		
		private boolean
		isForMonth(
			String	_year,
			String	_month ) 
		{
			return( year.equals( _year ) && month.equals( _month ));
		}
		
		private Map<String,List<Long>>
		getContents()
		{
			if ( contents == null ){
				
				File file = getCacheFile();
				
				if ( file.exists()){
					
					//System.out.println( "Reading cache: " + file );
					
					contents = FileUtil.readResilientFile( file );
					
				}else{
					
					contents = new HashMap<String, List<Long>>();
				}
			}
			
			return( contents );
		}
		
		private long[]
		getTotals(
			int		day )
		{
			List<Long> records = getContents().get( String.valueOf( day ));
			
			if ( records != null ){
				
				long[] result = new long[STAT_ENTRY_COUNT];
				
				if ( records.size() == STAT_ENTRY_COUNT ){
				
					for ( int i=0;i<STAT_ENTRY_COUNT;i++){
						
						result[i] = (Long)records.get(i);
					}
				}
				
				return( result );
			}
			
			return( null );
		}
		
		private long[]
 		getTotals(
 			int		day,
 			long	start_offset )
 		{
			if ( start_offset == 0 ){
				
				return( getTotals( day ));
				
			}else{
			
	 			List<Long> records = getContents().get( day + "." + start_offset );
	 			
	 			if ( records != null ){
	 				
	 				long[] result = new long[STAT_ENTRY_COUNT];
	 				
	 				if ( records.size() == STAT_ENTRY_COUNT ){
	 				
	 					for ( int i=0;i<STAT_ENTRY_COUNT;i++){
	 						
	 						result[i] = (Long)records.get(i);
	 					}
	 				}
	 				
	 				return( result );
	 			}
	 			
	 			return( null );
			}
 		}
		
		private void
		setTotals(
			int		day,
			long[]	totals )
		{
			List<Long>	records = new ArrayList<Long>();
			
			for ( Long l: totals ){
				
				records.add( l );
			}
			
			getContents().put( String.valueOf( day ), records );
			
			dirty	= true;
		}
		
		private void
		setTotals(
			int		day,
			long	start_offset,
			long[]	totals )
		{
			if ( start_offset == 0 ){
				
				setTotals( day, totals );
				
			}else{
				
				List<Long>	records = new ArrayList<Long>();
				
				for ( Long l: totals ){
					
					records.add( l );
				}
				
				getContents().put( day + "." + start_offset, records );
				
				dirty	= true;
			}
		}
		
		private boolean
		isDirty()
		{
			return( dirty );
		}
		
		private void
		save()
		{
			File file = getCacheFile();

			file.getParentFile().mkdirs();
			
			//System.out.println( "Writing cache: " + file );
			
			FileUtil.writeResilientFile( file, contents );
			
			dirty = false;
		}
	}
	
	public static void
	main(
		String[]	args )
	{
		try{
			LongTermStatsImpl impl = new LongTermStatsImpl( new File( "C:\\Test\\plus6b\\stats" ));
			
			SimpleDateFormat local_format = new SimpleDateFormat( "yyyy,MM,dd" );
			
			Date start_date = local_format.parse( "2013,07,10" );
			Date end_date 	= local_format.parse( "2013,07,16" );
			
			long[] usage = 
				impl.getTotalUsageInPeriod( 
					start_date, 
					end_date,
					new RecordAccepter()
					{
						public boolean 
						acceptRecord(
							long timestamp) 
						{
							System.out.println( new Date( timestamp ));
							
							return( true );
						}
					});
			
			System.out.println( getString( usage ));
		
			System.out.println( getString(impl.getTotalUsageInPeriod( PT_CURRENT_HOUR, 1 )));
			System.out.println( getString(impl.getTotalUsageInPeriod( PT_SLIDING_HOUR, 1 )));
			//System.out.println( getString(impl.getTotalUsageInPeriod( PT_CURRENT_WEEK )));
			//System.out.println( getString(impl.getTotalUsageInPeriod( PT_CURRENT_MONTH )));
			
		}catch( Throwable e ){
			
			e.printStackTrace();
		}
	}
}
